// src/main/java/com/dbms/storage/BPlusTreeInternalPage.java
package com.dbms.storage;

import java.nio.ByteBuffer;

/**
 * B+树内部节点页面实现类，继承自BPlusTreePage。
 * 内部节点的核心作用：存储「键-子页面ID」映射对，引导查找路径（指向子节点，子节点可为内部节点或叶子节点）。
 * 页面结构：[页面头部(继承自BPlusTreePage)] + [内容区(键-子页面ID对数组)]
 * 注意：内容区中，索引0仅存储「子页面ID」（无对应有效键），从索引1开始存储完整的「键-子页面ID」对（键用于划分子节点的键范围）
 */
public class BPlusTreeInternalPage extends BPlusTreePage {

    /** 内容区起始偏移量：跳过页面头部（HEADER_SIZE由父类BPlusTreePage定义，存储size、maxSize等元数据） */
    private static final int CONTENT_OFFSET = HEADER_SIZE;
    /** 单个键的字节大小：Integer类型占4字节 */
    private static final int KEY_SIZE = 4;
    /** 单个子页面ID的字节大小：int类型占4字节 */
    private static final int PAGE_ID_SIZE = 4;
    /** 单个「键-子页面ID」对的总字节大小：键(4) + 页面ID(4) = 8字节 */
    private static final int PAIR_SIZE = KEY_SIZE + PAGE_ID_SIZE;

    /**
     * 构造方法：将底层Page对象包装为B+树内部节点页面
     * @param page 底层存储页面（包含原始字节数据）
     */
    public BPlusTreeInternalPage(Page page) {
        this.page = page; // 赋值父类的page成员变量
        this.buffer = ByteBuffer.wrap(page.getData()); // 将页面字节数据包装为ByteBuffer，便于随机读写
    }

    /**
     * 初始化内部节点页面的元数据
     * @param maxSize 内部节点的最大容量（最大可存储的「键-子页面ID」对数量）
     */
    public void init(int maxSize) {
        // 调用父类BPlusTreePage的init方法，初始化页面头部（size=0、maxSize、页面类型=INTERNAL_PAGE等）
        super.init(page, maxSize, PageType.INTERNAL_PAGE);
    }

    /**
     * 二分查找目标键在当前内部节点中的「插入索引」（核心查找逻辑）
     * 作用：找到小于等于目标键的最大键对应的索引，用于后续定位子页面ID
     * 注意：内部节点的有效键从索引1开始，因此查找范围是 [1, size-1]
     * @param key 目标查找键
     * @return 目标键的插入索引（小于等于目标键的最大键的索引）
     */
    @Override
    public int keyIndex(Integer key) {
        int low = 1; // 有效键的起始索引（索引0无有效键）
        int high = getSize() - 1; // 有效键的结束索引（size是当前节点存储的「键-子页面ID」对总数）

        // 二分查找循环：缩小范围直到low > high
        while (low <= high) {
            // 计算中间索引（避免low+high溢出）
            int mid = low + (high - low) / 2;
            // 比较中间索引的键与目标键
            if (getKey(mid).compareTo(key) > 0) {
                // 中间键大于目标键，目标键在左半区
                high = mid - 1;
            } else {
                // 中间键小于等于目标键，目标键在右半区
                low = mid + 1;
            }
        }
        // 循环结束后，high即为小于等于目标键的最大键的索引，low-1等价于high
        return low - 1;
    }

    /**
     * 根据索引获取内部节点中的键
     * @param index 键的索引（1<=index<size，索引0无有效键）
     * @return 索引对应的键
     */
    @Override
    public Integer getKey(int index) {
        // 计算键在字节缓冲区中的偏移：内容区起始 + 索引*键值对大小（每个对占PAIR_SIZE字节）
        return buffer.getInt(CONTENT_OFFSET + index * PAIR_SIZE);
    }

    /**
     * 根据索引设置内部节点中的键，并标记页面为脏页
     * @param index 键的索引（1<=index<size）
     * @param key 要设置的新键
     */
    public void setKey(int index, Integer key) {
        // 计算偏移并写入键
        buffer.putInt(CONTENT_OFFSET + index * PAIR_SIZE, key);
        // 标记页面为脏页（通知缓冲池管理器后续需将修改刷盘）
        markDirty();
    }

    /**
     * 根据索引获取内部节点中的子页面ID
     * @param index 子页面ID的索引（0<=index<size，索引0对应第一个子节点，无前置键）
     * @return 索引对应的子页面ID
     */
    public int getPageId(int index) {
        // 计算子页面ID的偏移：内容区起始 + 索引*键值对大小 + 键的大小（键在前，页面ID在后）
        return buffer.getInt(CONTENT_OFFSET + index * PAIR_SIZE + KEY_SIZE);
    }

    /**
     * 根据索引设置内部节点中的子页面ID，并标记页面为脏页
     * @param index 子页面ID的索引（0<=index<size）
     * @param pageId 要设置的新子页面ID
     */
    public void setPageId(int index, int pageId) {
        // 计算偏移并写入子页面ID
        buffer.putInt(CONTENT_OFFSET + index * PAIR_SIZE + KEY_SIZE, pageId);
        // 标记页面为脏页
        markDirty();
    }

    /**
     * 根据目标键查找对应的子页面ID（内部节点的核心导航方法）
     * 作用：给定键，找到应递归查找的子节点页面ID
     * @param key 目标查找键
     * @return 对应的子页面ID
     */
    public int lookUp(Integer key) {
        // 1. 先通过keyIndex找到小于等于目标键的最大键的索引
        int index = keyIndex(key);
        // 2. 该索引对应的子页面ID就是目标键所在的子节点（内部节点的索引i对应子节点存储<=getKey(i)的键）
        // 注：若index=0（无对应键），则返回索引0的子页面ID（存储最小范围的键）
        return getPageId(index);
    }

    /**
     * 初始化新的根节点（用于根节点分裂场景）
     * 场景：原根节点（叶子/内部）分裂后，新建内部节点作为新根，需存储「旧根-中间键-新节点」的映射
     * @param oldPageId 分裂前原节点的页面ID（新根的左子节点）
     * @param key 分裂产生的中间键（用于划分左右子节点的键范围）
     * @param newPageId 分裂产生的新节点的页面ID（新根的右子节点）
     */
    public void populateNewRoot(int oldPageId, Integer key, int newPageId) {
        // 新根节点初始存储2个「子页面ID」和1个「键」（size=2表示有2个孩子，对应1个键）
        setSize(2);
        // 索引0：左子节点（原节点）的页面ID（无前置键）
        setPageId(0, oldPageId);
        // 索引1：中间键（划分左右子节点的键）
        setKey(1, key);
        // 索引1：右子节点（新节点）的页面ID
        setPageId(1, newPageId);
    }

    /**
     * 【已修改】将当前节点的后半部分数据移动到新节点（用于节点分裂），并返回需提升到父节点的中间键
     * 逻辑：1. 计算分裂位置和中间键 2. 移动后半数据到新节点 3. 更新当前节点大小
     * @param recipient 接收数据的新兄弟节点（空的内部节点）
     * @return 需提升到父节点的中间键（划分当前节点与新节点的键范围）
     */
    public Integer moveHalfTo(BPlusTreeInternalPage recipient) {
        // 1. 计算分裂起始位置：(maxSize+1)/2，确保分裂后左右节点大小均衡（左多右少或相等）
        // 例：maxSize=3 → (3+1)/2=2，当前节点保留前2个（索引0-1），新节点接收从2开始的数据
        int start = (getMaxSize() + 1) / 2;
        // 中间键：分裂位置start对应的键，需提升到父节点（划分当前节点与新节点的键范围）
        Integer middleKey = getKey(start);

        // 2. 计算需移动的「键-子页面ID」对数量：当前节点总大小 - (start+1)
        // 因为start对应的键要提升到父节点，不移动；从start+1开始的才是要移动的有效键
        int numToMove = getSize() - (start + 1);
        // 将当前节点start+1开始的numToMove个键值对，拷贝到新节点
        recipient.copyNFrom(page.getData(), CONTENT_OFFSET + (start + 1) * PAIR_SIZE, numToMove);

        // 3. 新节点的第一个子页面ID：当前节点start位置的子页面ID（因为start的键提升后，其对应的子节点归新节点）
        recipient.setPageId(0, getPageId(start));

        // 4. 更新当前节点的大小：保留前start个「键-子页面ID」对（索引0到start-1）
        setSize(start);

        // 返回中间键，供父节点插入
        return middleKey;
    }

    /**
     * 【已修改】从源字节数据拷贝指定数量的「键-子页面ID」对到当前节点（供moveHalfTo调用）
     * 注意：源数据是分裂节点的后半部分，无第一个子页面ID（需在moveHalfTo中单独设置）
     * @param source 源字节数据（来自分裂节点的页面数据）
     * @param offset 源数据的起始偏移（分裂节点中需拷贝数据的起始位置）
     * @param n 要拷贝的「键-子页面ID」对数量
     */
    public void copyNFrom(byte[] source, int offset, int n) {
        // 当前节点的当前大小（拷贝前的初始大小，通常为0，因recipient是新节点）
        int currentSize = getSize();
        // 拷贝逻辑：将源数据中n个键值对，拷贝到当前节点的CONTENT_OFFSET + (currentSize+1)*PAIR_SIZE位置
        // 原因：当前节点的键从索引1开始存储（索引0是子页面ID），需预留索引1的位置给第一个拷贝的键
        System.arraycopy(
                source, offset, // 源数据及起始偏移
                page.getData(), CONTENT_OFFSET + (currentSize + 1) * PAIR_SIZE, // 目标位置（预留索引0的子页面ID位置）
                n * PAIR_SIZE // 拷贝的总字节数（n个键值对，每个PAIR_SIZE字节）
        );
        // 增加当前节点的大小：拷贝了n个键值对，size += n
        increaseSize(n);
        // 注：第一个子页面ID已在moveHalfTo中单独设置，此处无需处理
    }

    /**
     * 在指定的旧子页面ID之后插入新的「键-子页面ID」对（用于父节点插入新分裂节点）
     * 逻辑：1. 找到旧子页面ID的索引 2. 移动数据腾出位置 3. 插入新键值对 4. 更新大小
     * @param oldPageId 参考的旧子页面ID（新键值对插入到该ID之后）
     * @param newKey 要插入的新键
     * @param newPageId 要插入的新子页面ID
     */
    public void insertNodeAfter(int oldPageId, Integer newKey, int newPageId) {
        // 1. 遍历找到旧子页面ID在当前节点中的索引oldPageIndex
        int oldPageIndex = 0;
        while (oldPageIndex < getSize()) {
            if (getPageId(oldPageIndex) == oldPageId) {
                break; // 找到旧子页面ID的索引，退出循环
            }
            oldPageIndex++;
        }

        // 2. 新键值对的插入索引：旧子页面ID的索引+1（插入到旧子节点之后）
        int insertIndex = oldPageIndex + 1;
        // 移动数据：将insertIndex及之后的键值对向后移动一个PAIR_SIZE，腾出插入位置
        System.arraycopy(
                page.getData(), CONTENT_OFFSET + insertIndex * PAIR_SIZE, // 源位置（从插入索引开始）
                page.getData(), CONTENT_OFFSET + (insertIndex + 1) * PAIR_SIZE, // 目标位置（向后移一位）
                (getSize() - insertIndex) * PAIR_SIZE // 移动的字节数（剩余键值对数量 * 每个对的大小）
        );

        // 3. 在腾出的位置插入新键和新子页面ID
        setKey(insertIndex, newKey);
        setPageId(insertIndex, newPageId);

        // 4. 增加当前节点的大小（插入了一个键值对）
        increaseSize(1);
    }
}